/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package GameLogic;

import Algorithm.KDTree;
import Algorithm.Pathfinding;
import GameData.Map;
import java.awt.Point;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

/**
 *
 * @author ix
 */

public final class Game implements IGame
{
    public enum GameState
    {
        BetweenWaves, InWave
    }
    
    final static double SPAWN_INTERVAL = 1500;
    GameState State;
    double NextWaveTimer;
    
    Tile[][] MapData;
    Point MapSize;
    boolean[][] BuildLayer;
    boolean[][] PathWalkMap;
    boolean[][] PathFlyMap;
    int LivesRemaining;
    
    Point SpawnPoint = new Point(0,0),
          EndPoint = new Point(0,0);
    
    int[] TowerTypeCount;
    
    Scenario GameScenario;
    
    Tower[] PlayerTowers = new Tower[200];
    Missile[] PlayerMissiles = new Missile[500];
    Mob[] Mobs = new Mob[200];
    int TowersCount = 0,
        MissilesCount = 0,
        MobsCount = 0;
    int PlayerGold = 0;
    int WaveNumber = -1;
    int MobsAlive = 0;
    int MobsToSpawn = 0;
    int PlayerKills = 0;
    double TimeToSpawn = SPAWN_INTERVAL;
    boolean isPlaying = true;
    KDTree TowerTree = new KDTree(null);
    ArrayList<Point> Waypoint = new ArrayList<Point>();
    Updatable Callback;
    
    //1-liners
    public GameState GetCurrentState() {
        return State;
    }
    public TowerType GetUpgrade(ITower t) {
        return ((Tower)t).type.upgradesTo;
    }
    public IMob[] GetMobs() {
        return (IMob[])Mobs;
    }
    public int GetMobsCount() {
        return MobsCount;
    }
    public int GetMobsAlive() {
        return MobsAlive;
    }
    public ITower[] GetTowers() {
        return (ITower[])PlayerTowers;
    }
    public int GetTowersCount() {
        return TowersCount;
    }
    public IMissile[] GetMissiles() {
        return (IMissile[])PlayerMissiles;
    }
    public int GetMissilesCount() {
        return MissilesCount;
    }
    
    public double GetTimeToWave() {
        return NextWaveTimer;
    }
    
    public int GetLives() {
        return LivesRemaining;
    }
    
    public void SetOnUpdate(Updatable c) {
        Callback = c;
    }
    
    public int GetCurrentLevel() {
        return WaveNumber;
    }
    
    public int GetCurrentGold() {
        return PlayerGold;
    }
    
    public int GetKills() {
        return PlayerKills;
    }
    
    
    private boolean IsBuildable(int x, int y)
    {
        return MapData[x][y].canBuild && !BuildLayer[x][y];
    }
    
    public boolean[][] GetTowerMap()
    {
        boolean[][] r = new boolean[MapSize.x][MapSize.y];
        for(int x = 0; x < MapSize.x; x++)
            for(int y = 0; y < MapSize.y; y++)
                r[x][y] = MapData[x][y].canBuild && !BuildLayer[x][y];
        return r;
    }
    
    void CalcAllPaths()
    {
        
        
    }
    
    void CalcPath(Mob m)
    {
        boolean[][] walkable;
        if(m.type.isFlying)
            walkable = PathFlyMap;
        else
            walkable = PathWalkMap;
        List<Point> l = Algorithm.Pathfinding.GetRoute(MapSize, walkable, new Point((int)m.x, (int)m.y), Waypoint.get(m.WaypointNum));
        if(l == null)
        {
            l = Algorithm.Pathfinding.GetRoute(MapSize, BuildLayer, new Point((int)m.x, (int)m.y), Waypoint.get(m.WaypointNum));
        }
        if(l == null)
            return;
        m.Path = new ArrayList<Point2D>();
        m.PathNum = 0;
        m.PathPos = 0;
        m.Path.add(new Point2D.Double(m.x, m.y));
        for(int i = 0; i < l.size(); i++)
            m.Path.add(new Point2D.Double((double)l.get(i).x + 0.5, (double)l.get(i).y + 0.5));
        m.PathLength = new double[m.Path.size() - 1];
        for(int i = 0; i < m.PathLength.length; i++)
            m.PathLength[i] = Pathfinding.GetDist(m.Path.get(i), m.Path.get(i+1));
    }
    
    public void SpawnMob()
    {
        Mob m = new Mob(GameScenario.MobList[WaveNumber]);
        m.g = this;
        Mobs[MobsCount++] = m;
        m.x = SpawnPoint.x + 0.5;
        m.y = SpawnPoint.y + 0.5;
        CalcPath(m);
    }
    
    public void StartNextWave()
    {
        if(MobsAlive > 0)
            return;
        WaveNumber++;
        if(GameScenario.MobList[WaveNumber] != null)
        {
            MobsToSpawn = GameScenario.MobCount[WaveNumber]-1;
            MobsAlive = MobsToSpawn+1;
            SpawnMob();

            State = GameState.InWave;
        }
        else
            EndGame(true);
        
    }
    
    public boolean isPlaying()
    {
        return isPlaying;
    }
    
    boolean hasWon;
    Runnable onGameFinish;
    
    public void setOnGameFinish(Runnable r)
    {
        onGameFinish = r;
    }
    
    public boolean getFinishState()
    {
        if(isPlaying)
            return false;
        return hasWon;
    }
    Thread updateThread;
    
    public boolean CanBuildTower(int size, int x, int y)
    {
        for(int ix = 0; ix < size; ix++)
            for(int iy = 0; iy < size; iy++)
            {
                if(!IsBuildable(x+ix,y+iy))
                    return false;
            }
        for(int i = 0; i < MobsCount; i++)
        {
            if(!Mobs[i].isDead && Mobs[i].x >= x && Mobs[i].x <= (double)x + (double)size/2 + 1 &&
               Mobs[i].y >= y && Mobs[i].y <= (double)y + (double)size/2 + 1)
                return false;
        }
        return true;
    }
    
    void UpdateTowerTree()
    {
        Tower[] t = new Tower[TowersCount];
        System.arraycopy(PlayerTowers, 0, t, 0, t.length);
        TowerTree = new KDTree(t);
        for(Tower tt : t)
            TowerAura.UpdateAuras(tt, TowerTree);
        for(int i = 0; i < MobsCount; i++)
            CalcPath(Mobs[i]);
    }
    
    public void DestroyTower(Tower t, boolean sell)
    {
        for(int ix = 0; ix < t.type.size; ix++)
            for(int iy = 0; iy < t.type.size; iy++)
            {
                BuildLayer[t.x+ix][t.y+iy] = false;
                PathWalkMap[t.x+ix][t.y+iy] = !MapData[t.x + ix][t.y + iy].canWalk;
            }
        TowerTypeCount[t.type.id]--;
        if(sell)
            PlayerGold += t.type.price * SELL_COEFFICIENT;
        for(int i = 0; i < TowersCount; i++)
            if(t == PlayerTowers[i])
            {
                PlayerTowers[i] = PlayerTowers[--TowersCount];
                UpdateTowerTree();
                break;
            }
    }
    
    public boolean BuildTower(TowerType tt, int x, int y)
    {
        Tower t = new Tower(tt);
        
        for(int i = 0; i < t.type.requires.size(); i++)
            if(TowerTypeCount[t.type.requires.get(i).id] == 0)
                return false;
        
        if(!CanBuildTower(t.type.size, x, y))
            return false;
        
        if(PlayerGold < t.type.price)
            return false;
        
        PlayerGold -= t.type.price;
        
        for(int ix = 0; ix < t.type.size; ix++)
            for(int iy = 0; iy < t.type.size; iy++)
            {
                BuildLayer[x+ix][y+iy] = true;
                PathWalkMap[x+ix][y+iy] = true;
            }
        
        t.x = x;
        t.y = y;
        t.g = this;
        
        TowerTypeCount[tt.id]++;
        
        PlayerTowers[TowersCount++] = t;
        UpdateTowerTree();
        return true;
    }
    
    
    
    private void UpdateTargets()
    {
        Tower t;
        double r2, d2;
        Point2D tPos;
        for(int i = 0; i < TowersCount; i++)
        {
            t = PlayerTowers[i];
            r2 = t.type.Range * t.type.Range;
            tPos = new Point2D.Double((double)t.x + (double)t.type.size/2,
                                      (double)t.y + (double)t.type.size/2);
            //remove old target if dead/away
            if(t.target != null && (t.target.isDead || Pathfinding.GetDist2(tPos, t.target.GetPosition()) > r2))
                t.target = null;
            //on cd, don't care
            if(t.curCooldown > 0)
                continue;
            //got old target?
            if(t.target == null)
            {
                double maxPath = 0;
                Mob newTarget = null;
                for(int mi = 0; mi < MobsCount; mi++)
                {
                    if(!Mobs[mi].isDead)
                    {
                        d2 = Mobs[mi].WaypointNum + Mobs[mi].PathPos;
                        if(Pathfinding.GetDist2(tPos, Mobs[mi].GetPosition()) <= r2 &&
                            d2 > maxPath)
                        {
                            maxPath = d2;
                            newTarget = Mobs[mi];
                        }
                    }
                }
                if(newTarget != null)
                    t.target = newTarget;
                else
                    t.target = null;
            }
        }
    }
    
    private void onUpdate()
    {
        for(int i = 0; i < MobsCount; i++)
        {
            Mob m = Mobs[i];
            if(m.isDead)
            {
                m.DecayTime -= updatePeriod;
                if(m.DecayTime <= 0)
                {
                    Mobs[i] = Mobs[--MobsCount];
                }
            }
        }
        if(State == GameState.BetweenWaves)
        {
            NextWaveTimer -= updatePeriod;
            if(NextWaveTimer <= 0)
                StartNextWave();
        }
        else
        {
            if(MobsAlive <= 0)
            {
                MissilesCount = 0;
                UpdateTargets();
                State = GameState.BetweenWaves;
                NextWaveTimer = 20000;
                return;
            }
            //spawn mobs?
            if(MobsToSpawn > 0)
            {
                TimeToSpawn -= (double)updatePeriod;
                if(TimeToSpawn <= 0)
                {
                    SpawnMob();
                    MobsToSpawn--;
                    TimeToSpawn = SPAWN_INTERVAL;
                }
            }
            //update towers' targets
            UpdateTargets();
            for(int i = 0; i < TowersCount; i++)
            {
                Tower t = PlayerTowers[i];
                if(t.MaxDamage > 0 && t.target != null && t.curCooldown == -1)
                {
                    Mob m = t.target;
                    //onAttack
                    double mX = (double)t.x + (double)t.type.size/2,
                            mY = (double)t.y + (double)t.type.size/2;
                    double or = Math.atan2(m.y - mY, m.x - mX);
                    double dmg = t.CalcDamage();
                    Missile ms = new Missile(m, t, t.missileSpeed, or, dmg, t.type.dmgType, mX, mY);
                    PlayerMissiles[MissilesCount++] = ms;

                    t.curCooldown = t.cooldown;
                }
                else
                {
                    t.curCooldown -= updatePeriod;
                    if(t.curCooldown <= 0)
                        t.curCooldown = -1;
                }
            }
            //update missiles
            for(int i = 0; i < MissilesCount; i++)
            {
                Missile ms = PlayerMissiles[i];
                if(ms.target.isDead)
                {
                    PlayerMissiles[i--] = PlayerMissiles[--MissilesCount];
                    continue;
                }
                double d = getDist(ms.x, ms.y, ms.target.x, ms.target.y);
                double dx = ms.target.x - ms.x,
                       dy = ms.target.y - ms.y;
                
                ms.x += dx * ms.curSpeed / updateFPS / d;
                ms.y += dy * ms.curSpeed / updateFPS / d;
                ms.curOrientation = Math.atan2(dy, dx);
                
                d -= ms.curSpeed / updateFPS;
                if(d <= 0.2)
                {
                    ms.target.DealDamage(ms.tower, ms.dmg, ms.dmgType);
                    PlayerMissiles[i--] = PlayerMissiles[--MissilesCount];
                }
            }
            //update mobs
            for(int i = 0; i < MobsCount; i++)
            {
                Mob m = Mobs[i];
                if(m.isDead)
                    continue;
                Buff.UpdateBuffs(m, (int)updatePeriod);
                MobAura.UpdateAuras(m, TowerTree);
                m.PathPos += m.MoveSpeed / updateFPS / m.PathLength[m.PathNum];
                if(m.PathPos > 1)
                {
                    m.PathNum++;
                    m.PathPos = 0;
                    
                    if(m.PathNum == (m.Path.size()-1))
                    {
                        m.WaypointNum++;
                        
                        if(m.WaypointNum < Waypoint.size())
                        {
                            CalcPath(m);
                        }
                        else
                        {
                            Mobs[i].isDead = true;
                            //remove the mob

                            //substract a live
                            LivesRemaining--;
                            MobsAlive--;
                            if(LivesRemaining == 0)
                                EndGame(false);
                            return;
                        }
                        //return
                    }
                }
                
                m.x = m.Path.get(m.PathNum).getX() + (m.Path.get(m.PathNum + 1).getX() - m.Path.get(m.PathNum).getX()) * m.PathPos;
                m.y = m.Path.get(m.PathNum).getY() + (m.Path.get(m.PathNum + 1).getY() - m.Path.get(m.PathNum).getY()) * m.PathPos;
                
            }
        }
        if(Callback != null)
            Callback.onUpdate();
    }
    
    public boolean canBuildTowerType(TowerType tt)
    {
        for(int i = 0; i < tt.requires.size(); i++)
            if(TowerTypeCount[tt.requires.get(i).id] == 0)
                return false;
        return true;
    }
    public ITower UpgradeTower(ITower it)
    {
        Tower t = (Tower)it;
        if(t.type.upgradesTo == null)
            return it;
        if(PlayerGold < t.type.upgradesTo.price)
            return it;
        int id;
        for(int i = 0; i < TowersCount; i++)
            if(t == PlayerTowers[i])
            {
                id = i;
                break;
            }
        //PlayerGold -= t.type.price;
        DestroyTower(t, false);
        BuildTower(t.type.upgradesTo, t.x, t.y);
        return (ITower)PlayerTowers[TowersCount - 1];
        
    }
    
    private double getDist(double ax, double ay, double bx, double by)
    {
        return Math.sqrt((ax-bx)*(ax-bx) + (ay-by)*(ay-by));
    }
    
    private void EndGame(boolean win)
    {
        updateThread.stop();
        isPlaying = false;
        
    }
    
    private class UpdateClass implements Runnable
    {
        public void run()
        {
            long lastRun;
            double RunTime;
            while(true)
            {
                lastRun = System.nanoTime();
                
                onUpdate();
                
                RunTime = (System.nanoTime() - lastRun) / 1000000;
                if(RunTime < updatePeriod)
                {
                    try 
                    {
                        Thread.sleep((int)(updatePeriod - RunTime));
                    } catch (Exception e) { }
                }
            }
        }
    }
    
    
    int updateFPS;
    double updatePeriod;
    //creates a new instance of a Game with given FPS (updates per second)
    public Game(Scenario sc, int updFPS)
    {
        GameScenario = sc;
        
        //
        LivesRemaining = sc.StartLives;
        PlayerGold = 1500;
        TowerTypeCount = new int[sc.TowerList.length];
        //load map
        MapData = Map.LoadFromFile(sc.MapFile, SpawnPoint, EndPoint, Waypoint);
        MapSize = new Point(MapData.length, MapData[0].length);
        BuildLayer = new boolean[MapSize.x][MapSize.y];
        PathFlyMap = new boolean[MapSize.x][MapSize.y];
        PathWalkMap = new boolean[MapSize.x][MapSize.y];
        
        isPlaying = true;
        
        for(int x = 0; x < MapSize.x; x++)
            for(int y = 0; y < MapSize.y; y++)
            {
                PathFlyMap[x][y] = !MapData[x][y].canWalk;
                PathWalkMap[x][y] = !MapData[x][y].canWalk;
            }
                
        
        //if(true == true) return;
        State = GameState.BetweenWaves;
        NextWaveTimer = 50;
        
        //start updating thread!
        updateFPS = updFPS;
        updatePeriod = 1000 / updFPS;
        updateThread = new Thread(new UpdateClass());
        updateThread.start();
    }
}
